华为新贵!方舟编译器的荣光和使命
本文由MO编辑部出品,文字超1万,查阅的资料多达上百万文字,并且与华为方舟编译器的专家做过深度访谈。文章高度概括了波澜壮阔的软件产业发展史,解析了华为在编译器和手机系统方面做的多年准备。
2017年5月的一个凌晨,华为某实验室里,方舟编译器上第一个Java程序“Hello, World”跑通了。
01
这个故事,先从计算机的语言说起。
事实上,人类文明中最早出现的文字就是数字,比完整表意的文字早500-1000年。
随着人类社会的发展,社会结构开始变得复杂,征税必须先收集国民收入、财产、付款、欠款、债务、罚款等数据,远远超过人脑的记忆系统。于是,数字诞生了。
5000年前,苏美尔人文明(今科威特及邻近地区)以6和10作为基数,用符号表达1、10、60、600、以及3600。今天,我们仍然常见以6为基数的数字,例如圆有360度,一天有24小时。
早在17世纪,英国借助强大的经济实力和海军力量拥有了众多的殖民地,建立了全球霸主地位。舰队使用的航海表计算复杂,人工计算的数据经常有误,百年间导致大量的船只触礁沉没。
直到第一次工业革命爆发,英国政府找到了数学家巴贝奇(Charles Babbage)来修正航海表。巴贝奇通过十年的努力,研制成功了“差分机”。用蒸汽机时代的机械齿轮来存储计算过程中的数据,大大提升了航海表数据的计算速度和准确率。
同期,他开始了另外一种新机器——分析机的设计。在开发分析机的过程中,巴贝奇冥思苦想,发现乘法是加法的重复,除法是减法的重复,减法可以用加法来代替,这样就只要设计一个加法运算器即可。基于这种假设,计算机器只需要具备两个功能即可运作:1)判断两个数的大小;
2)不断重复执行加法计算。
所有的运算转化为最简单的判断和计算,1表示Yes(开),0表示No(关),计算机用0和1不断重复计算。这就是计算机使用“0”和“1”二进制语言的理论基础。
值得一提的是,巴贝奇发明分析机不久之后,英国数学家乔治·布尔于1854年发表了“布尔代数学”,提出了逻辑学的二元运算,为现代电子计算机提供了另外一个重要的理论基础。
回到分析机,巴贝奇借鉴了法国纺织机上使用的穿孔卡片。卡片上的孔眼排列,控制纺织机的梭子,能够不断重复的织出各种各样美丽图案的布匹。巴贝奇在著名诗人拜伦之女Ada Lovelace的帮助下,发现穿孔卡片不仅可以用来记录数据,还能用来控制机器工作的指令。在卡片上打上不同排列的孔,机器便有不同的工作程序,这就是现代计算机的软件设计思想。
基础研究与基础教育是产业诞生和振兴的根本。这也是为什么今天的英国,仍然在数学和通信领域拥有非常广泛的影响力。
1884年,美国统计专家赫尔曼·何乐礼(Herman Hollerith)借鉴穿孔卡原理发明了电动制表机,用于美国人口统计,与巴贝奇的分析机具有异曲同工之妙。他把所有需要调查的项目依次固定在一张硬纸卡上,然后将统计的居民个人信息在相应位置打孔,用来表示“Yes”。当统计机器的探针撞到卡片上的“孔”,就会接通电流,计数装置往前进一个刻度。何乐礼博士正是采用了二进制的原理,来高效统计美国人口信息。
何乐礼博士随后创建了制表机公司,也就是赫赫有名的IBM公司的前身。
1935年,著名的现代计算机科学之父和人工智能之父,艾伦·图灵提出了著名的图灵机模型,为现代计算机的逻辑工作方式奠定了理论基础。
1937年,美国哈佛大学著名计算机专家霍德·艾肯在IBM公司的资助下,借鉴上面提到的英国数学家巴贝奇的分析机理论,于1944年研制成功世界第一台机电计算机——Mark-I型。IBM把这台计算机送给了哈佛大学,并一直使用到1959年,为培养早期的计算机科学家做出了巨大贡献。
另外一位学者就没有这样幸运了。美国爱荷华州立大学理论量子物理学的约翰·阿塔那索夫(John Atanasoff)副教授,苦于没有机器能够计算出量子物理中许多变量的求解。同样是在1937年,他寻求IBM公司的资助,但被断然拒绝,于是他横下决心一定要研制一台更好的计算机器。他利用当时正在发展的真空晶体管,用电子脉冲来表示“0”和“1”,并在电气工程专家克利福特·贝瑞(Clifford Berry)的帮助下,两人于1940年底研制成功了世界第一台电子计算机,并用两个人的名字将其命名为ABC(Atanasoff-Berry Computer)。
阿塔那索夫开辟了一种计算机的全新路径,为现代电子计算机的架构奠定了不可磨灭的基础。今天的华为在内部强调研发上要多路径突破,或许也曾受此启发。
1940年底,他结识了美国物理学家莫克利(JohnW.Mauchly)。莫克利在了解和参观了ABC计算机后,联合宾夕法尼亚大学莫尔学院的电气专家艾克特(J.PresperEckert),在国防部的资助下和冯·诺依曼博士的指导下,于1945年研制成功了世界第一台通用电子计算机ENIAC。虽然这台计算机采用的是十进制,但当时毫不影响它的计算能力,它大大缩短了美国“曼哈顿计划”的原子弹研发时间,加速了二战的结束。
1945年6月,冯·诺依曼将ENIAC电子计算机的十进制改回成二进制,并提出计算机内部存储器存储程序的概念,从而奠定了现代电子计算机理论的架构。
02
实际上,在电子计算机诞生之前,计算机这个角色是由女人来扮演的。这些女人被称为Computers,大多数计算都是她们手工完成的。
世界第一段程序,是上面提到的巴贝奇的师妹著名诗人拜伦之女Ada Lovelace在打孔卡上写出来的。程序设计人员都要把程序编排成010101这样的形式,在纸上打孔,再送到机器里去读。这减少了相当多的工作,写一个卡片能使用相当久,也避免了重复造轮子。
因此,当程序员开始在卡片上写这些程序时,人们开始构建程序库(libraries)。随着时间的推移,大部分的程序渐渐的有了一些规则手册来指导编写。但还是很繁琐,毕竟编写和记忆0和1的各种组合,对于很多人来说相当于是“天书”。
一个传奇的人物出现了。
美国海军的传奇天才格雷斯·霍珀(Grace Hopper),在海军服役时,曾被派到哈佛大学,与艾肯博士一起工作,负责为美国国防部资助的Mark-II型机电计算机编制程序。在二战结束后,她加入了发明ENAIC电子计算机的莫克利和艾克特创办的“电子控制公司”,开始了她改变世界的光辉旅程——打开编译器和编程语言的新世界。
Grace在加入莫克利和艾克特的公司,便产生了一种想法。她想设计一种程序,让人可以用类似英文的语法,把想做的事写下来,然后用这个程序把英文翻译成机器能执行的语言,交给机器去执行。
她把这个革命性的想法付诸实践,发明了世界上第一个编译器A-0。这是编译器的始祖,也是现代编程语言的始祖。
1952年,Grace和莫克利研究出了一种较为接近自然语言的计算机语言——汇编语言Flow-Matic。汇编语言本质上是使用助记符来代替机器语言01010101,进了一大步,但这种语言对计算机硬件依赖很大。不同的计算机,汇编语言不相通。
IBM公司在1946年后从制表机全面转型电子计算机市场,并投入巨资招揽了世界众多顶尖的计算机人才。其中一位叫约翰·贝克斯(John W. Backus),这位出身证券经纪人家庭的富二代,酷爱数学,加入IBM公司三年后,发明了一种快速编程的FORTRAN语言。同时,他深入研究了Grace发明的Flow-Matic的编译器,应用到FORTRAN的编译器中。
FORTRAN的问世,在计算机史上具有划时代的意义。它是世界第一个高级编程语言,使计算机语言从原始的低级汇编语言走到人人易懂的境界,计算机不再是科学家的专利。FORTRAN的诞生,孕育了软件产业。此后,计算机高级编程语言进入蓬勃发展的时代。
随后,IBM公司开发出了的ALGOL高级语言,人工智能之父约翰·麦卡锡(John McCarthy)发布了人工智能设计语言LISP,Grace奶奶也在Flow-Matic汇编语言的基础上开发了COBOL语言。
FORTRAN适合科学计算,ALOGOL适合事务处理,LISP适合人工智能计算,COBOL适合商业处理。这四种高级语言虽然比汇编语言更简单,但对于非计算机专业人员,编程仍难度较大。于是,Dartmouth学院的两位教授联手开发一种更简单的编程语言BASIC(初学者的全方位符式指令代码)。这是一种不需要编译的语言,只需要经过解释器执行即可,初期的Android智能手机软件借鉴了这种机制。BASIC语言也是目前被广泛使用的Visual Basic语言的鼻祖,目前在微软Excel的“宏”中可以直接使用。
美国编程语言的开发热潮你追我赶,其他国家也不甘示弱。
1963年,英国剑桥大学推出了CPL语言,后又推出了简化的BCPL语言。
1970年,美国贝尔实验室的肯·汤普逊(Ken Thompson)和丹尼斯·利奇(Dennis Ritchie)在BCPL语言的基础上,推出了更加简单的B语言(取BCPL第一个字母),后又简化出了C语言(取BCPL第二个字母)。这是目前世界上最常用的编程语言之一,甚至可以说是第一大编程语言。
C++:1983年正式发布。由贝尔实验室基于C语言改良,所以C++是完全兼容C语言的。
Objective-C:1980年代发明。1988年,斯蒂夫·乔布斯(Steve Jobs)在NeXT公司时买下了 Objective-C 语言的授权,后来成为苹果公司MAC电脑和iPhone手机的程序设计语言。
Java:1995年5月发布。SUN公司为了在电视机顶盒等电子产品智能化过程中抢占先机,解决跨平台的问题,所以开发了Java语言。但Java需要借助虚拟机机制来解释源代码并调度硬件资源。安卓系统使用的基础语言就是Java。
C#:2000年6月,由微软发布。C#与Java有着惊人的相似,也需要借助类似于虚拟机的Framework来运行;不同的是,Java支持所有平台,而C#只支持Windows和Linux系统。
GO:谷歌在2009年发布。GO语言主要用作服务器端和云计算开发。
Swift:2014年由苹果公司发布,可以和Objective-C语言混合使用。
其中,C/C++是编译语言,即在程序员写完程序后,通过编译器直接编译成机器码,安装到相应的硬件设备上即可直接运行。
Java/C#是预编译语言,就是需要先在开发者环境中将源代码(Source Code)转换成字节码(Byte Code),然后在设备上运行时再将字节码编译或解释成硬件能听得懂的机器码。将源代码转换成字节码的过程,就叫预编译。
编译器的前世今生
Windows系统
使用CL编译器。它直接集成在Visual Studio或Visual C++的开发者环境中,一般不单独使用。
Linux系统
使用开源的GCC编译器。GCC,由自由软件运动的精神领袖理查德·马修·斯托曼(Richard Matthew Stallman, RMS)在上个世纪80年代创立。它原本只处理C语言,后续扩展可以处理C++、Objective-C、Java等其他语言。GCC被认为是跨平台软件的编译器首选。
苹果Mac和iOS系统
最初使用GCC编译器,现已替代为Clang + LLVM。
为什么苹果公司要换编译器呢?
因为苹果发现开源的GCC开发者根本使唤不动,他们不愿意专门为了苹果公司的要求优化和改进GCC代码,所以苹果将编译器后端直接替换为LLVM(Low Level Virtual Machine,底层虚拟机),并且将读研时(2003年)发明LLVM的天才少年克里斯·拉特纳(Chris Lattner)招入麾下。克里斯进入了苹果之后,大幅度优化和改进LLVM以适应Objective-C的语法变革和性能要求,同时发起了CLang项目来完全替代GCC。
今天,GCC + LLVM 已经被替换成了 Clang +LLVM。LLVM负责编译器后端,用来处理代码优化和跨平台,而Clang负责前端,只需将程序源代码转换成LLVM可以看得懂的IR(Intermediate Representation,中间语言)即可。
另外,上文提到的Swift语言,则是这位天才少年克里斯为苹果公司贡献的第三个重要作品。
安卓系统
安卓主要使用预编译的Java语言开发,最初版本通过虚拟机运行,不需要编译器,后续版本加入了JIT和AOT编译机制(下一部分将详细展开)。
除了上述编译器之外,还有两个著名的编译器值得一提。Intel的ICC编译器
ICC编译器,全称Intel C++ Compiler,是Intel开发的C/C++/Fortran编译器套装,适用于Linux、Microsoft和Mac OS X操作系统,广泛应用于高性能计算、分布式计算等商业计算领域。
SGI等公司推出的Open64编译器
SGI(S代表超级计算机,G代表图形工作站,I代表具有突破性的洞察力)生产的超级计算机,主要应用于巨大的实验室,采用Open64编译器。
安卓的四大命门
第一个命门
Java的“虚拟机”
前面提到,Java为了能够实现跨平台操作,便借助虚拟机来调度硬件平台资源。在虚拟机里,还需要集成翻译器或者编译器,来将Java的字节码(即中间代码)解释成机器听得懂的机器语言,或者直接编译成机器直接执行的010101的机器码。
2008年,Android 1.0刚发布的时候,使用的是一个叫Dalvik的虚拟机,里面集成了一个解释器,每次用户在安卓手机上运行APP时,就会叫醒这个解释器,来给安卓的硬件解释APP想要干嘛。这就相当于新闻发布会,发言人讲一句自己的母语,然后再由专业翻译将其翻译成外国记者听得懂的语言,效率非常低下,一个小时可能也问不了几个问题。
谷歌意识到这个问题严重拖了安卓手机的后腿,所以通过一年多的努力,在2010年中发布了2.2版本,引入了JIT(Just in Time,即时编译)机制。JIT比较聪明,当用户在安卓手机运行APP时,会同时将用户经常使用的功能编译为机器能直接执行的010101机器码,不用每一句每一句的去翻译。当出现不常用的功能时,再把解释器叫起来翻译。
JIT虽然变聪明了一点,但是每次启动APP都要先编译一次,不能一劳永逸。加上Dalvik虚拟机性能比较落后,所以谷歌在2014年10月推出了Android 5.0版本,将虚拟机从Dalvik替代成ART(Android Run Time),同时把JIT的编译器替代成AOT (Ahead of Time)。意思就是说,APP在下载后安装到手机上时同时把能编译的代码先编译成机器听得懂的101010。剩下不太好翻译的代码,就在用户使用时再叫醒解释器来翻译。AOT相比JIT的好处,就是不用每次打开APP都需要先编译一遍。但是,坏处就是用户安装APP的时间有点长。
越来越多的用户吐槽为什么安装一个APP也慢吞吞。于是,谷歌在2017年Android 7.0又做了一点改进,安装时先不编译中间代码,而是在用户空闲时将能够编译成机器码的那部分代码,通过AOT编译器先静态编译了。如果AOT还没来得及编译或者不能编译,再叫醒JIT+解释器两个难兄难弟来顶住。这种机制,相当于用时间换空间,既缩短了用户安装APP的等待时间,又将虚拟机里编译器和解释器能做的优化提升到最大效率了。
很多人以为华为方舟编译器就是Android 7.0的ART虚拟机,其实不然。无论是编译器还是解释器,只是在虚拟机上打补丁。手机上的虚拟机+编译器+解释器本身不仅占用硬件资源,还无法最大发挥软件运行性能。正因如此,所以绝大部分手机厂商只能无奈的通过简单粗暴提升安卓手机的内存和存储空间,来弥补虚拟机的弊端。
第二个命门
Java的“原罪”——额外的JNI开销
目前95%的TOP应用都是使用Java和C/C++等多种语言混合开发而成。Java和C/C++属于两种不同架构的语言,各有自己的使用规范。为了APP正常运行,它俩之间需要互通有无,这个互通有无的接口就是JNI。在数据访问、函数调用、生命周期维护、异常处理等方面都需要这两种代码互相调用。这就意味着手机硬件资源要分配一部分给JNI去做调度。不仅占用了硬件资源,而且这种机制本身就效率较低。
这便是额外的JNI开销。
第三个命门
代码优化空间有限
第四个命门
Java现有内存回收机制易造成间歇性卡顿
十余年间,通过安卓系统的持续优化,以及内存的不断加持,安卓手机构筑了足够流畅的用户体验。
但是安卓的四大命门,如同达摩克利斯之剑,悬在安卓厂商的头上。华为科学家和工程师们,对此持续攻关了十年,只为再造安卓。
十年方舟,再造安卓
2017年5月,方舟编译器上第一个Java程序“Hello World”跑通。
2017年8月的一个凌晨,在华为的一个实验室里,项目组已经连续数日24小时不间断攻关,却始终看不到success的返回信息。绝望之下,工程师把所有通信数据打印出来,逐个字节排查,最终发现有一处字节的顺序不一致。纠正后,华为方舟跑通了第一个安卓后台服务DiskStatus,这标志着对安卓的换心手术进入了实操阶段。
2018年春节前一周,方舟编译器跑通安卓系统所有后台服务,并成功移植到手机。当晚,所有人聚集在实验室的机房中,等待首个开机画面加载成功的神圣时刻。秒针滴答,如同过了一个世纪那么久,屏幕终于点亮。
欢呼,拥抱,一蹦三尺高,项目组成员们到底没忍住,留下了激动的泪水。
接下来,项目组在除夕前夜启动了方舟编译手机的Beta测试。大年初一清晨,总架构师发来了第一条经编译器编译的运行程序发出来的拜年消息:
春节快乐,方舟大吉!
当P30发布会上“方舟”甫一出现,就在中国软件行业“炸”了锅。无数软件从业者从一开始的质疑,到弄清方舟真相后的惊叹……这些都是对华为软件工程师们十年如一日、一点一滴“啃硬骨头”的致敬!
实际上,方舟已经不是传统意义上从高级语言到机器码的“万能翻译”,更是一个编译运行系统。
一方面,方舟编译器首次在Java领域将虚拟机干掉了,也是软件史上首次将Java/C/C++等混合代码一次编译成机器码直接在手机上运行,彻底告别Java的JNI额外开销,也彻底告别了虚拟机GC内存回收带来的应用进程掉线,使操作流畅度大幅提升。如果说目前最新的安卓系统是和谐号动车,那么经方舟编译的安卓系统便是高铁,是“复兴号”。
华为手机直接通过方舟编译器替换了Android system-server的所有后台服务,这一项就已经足够让华为EMUI比其他安卓系统更快一步。根据华为官方测试,方舟编译器提升手机系统操作流畅度高达24%,系统响应性能提升44%。入手P30系列的用户,应该已经体验到了。
06
方舟编译器从立项开始,就是要彻底干掉虚拟机。这在历史上,是从来没有人做过的。
SUN公司当初发明Java语言,就是借助虚拟机的这个“万能中转站”,屏蔽掉各种硬件的差异,方便同一个代码在多个平台运行,所以Java是目前最流行的高级编程语言之一。这么多年,从来没有人能够干掉虚拟机,说明难度非常之大。
那么方舟编译器,是如何完全干掉虚拟机的呢?
最关键的就是要能够将Java代码直接编译成010101的机器码。
这种设想在理论是可行的,但是从现实来讲太难了。历史上有过一些尝试,比如现在大行其道的安卓ART虚拟机。这些尝试能够提前翻译Java语言里很多的静态语义,但是多数的动态语义,仍然搞不定,还得交给虚拟机。
所谓静态语义,我们可以理解为确定的语言和意思,比如“我是张三”。而动态语义,则需要结合上下文去理解,比如“能穿多少就穿多少”,到底穿多还是穿少,要看是冬天还是夏天了。例如大家熟悉的微博、京东等APP的开屏广告就需要用到动态语义。像编译静态语义一样去编译动态语义,很多知乎大神认为根本就不可能。
代码“万国通”
95%的Top应用都是Java/C/C++等混合语言编写而成。方舟编译器的第二个使命,就是干掉混合语言互相调用带来的JNI开销。
华为方舟编译器团队基于多个编程语言的深刻理解和大量研发积累,将混合语言破天荒实现了统一的中间表示IR,这就相当于同一个人能够理解全世界的语言,所以我们把这个革命性突破叫做,代码“万国通”。
那么方舟编译器是如何使用统一的IR来表示各种语言呢?
IR是用来表示代码的数据结构,它是编译器的各模块以及相关工具之间用来传递信息的“协议和通用语言”,也是程序变换和编译优化各种算法的承载体。它是编译器的“大脑”,直接决定了编译器的最终效果。它的难度是最高的。
华为方舟编译器团队对IR进行了长达五年的精雕细琢,逐渐摸索出“大脑”里每一条神经、每一个神经元的信号规律,并在此基础上发明了一套核心专利,使得不同语言代码在开发者环境中能够统一编译成同一套可直接执行的机器码,从而彻底消除混合语言互相调用的开销。
给代码装上飞机发动机
除了IR,编译器还有一个非常重要的价值,就是代码优化。
华为方舟编译器,直接将代码优化从手机环节搬到了开发者环境,未来还可能搬到云端。利用开发者环境更强大的算力,可以实现更先进和精细的优化算法,来达到更强大的优化效果,在很多特定场景代码优化的提升甚至是颠覆性的。
这相当于给APP在方舟里装上飞机发动机,让你的APP运行如飞!
值得一提的是,开发者使用方舟编译器,并不需要改变原来的编码习惯。开发者可以自行开发代码优化算法,也可以仅通过方舟编译器预置的算法进行代码优化。未来,华为还将提供代码调优工具,开发者可以选择根据工具的优化建议来调整代码,和方舟编译器配合获得更优的执行效果。
Stop World,安卓卡顿再见
方舟的第四个使命,就是解决安卓虚拟机GC内存回收带来的“Stop World”。
目前安卓手机内存资源不够用的时候,GC直接叫停所有应用,所以偶尔会遭遇莫名卡顿。
方舟编译器采用了引用计数法(RC,Reference Counting)来进行内存的实时回收,并且配合使用了专门的消除环算法(消除对象互相引用带来的无法回收问题),来避免GC集中式回收带来的系统卡顿。相比GC,方舟的内存回收是实时的而非集中式的,且不需要暂停应用进程,这样便大大消除了卡顿。
如果把内存回收比作成打扫房间,那么GC的策略是专门有一个卫生员,看房间里垃圾太多了,就把所有人请出去,打扫完了再让大家回来。而方舟的RC则是每个人收拾自己的垃圾,用完就清理,保持清洁的同时不影响人的正常活动。
软件有一个大家很熟悉的死循环,就是电脑被一个无限循环的运行程序把计算机资源占光。
这种“死循环”在软件中叫“环引用”。为了从机制避免手机内存被环引用“吃掉”,方舟编译器引入annotation的“告警”标示,对基础类的环进行标注。当然,Java程序员也可以对业务代码中的环进行标注。经过丰富的实践验证,方舟这种机制可减少大部分程序中环的出现。
另外一方面,方舟编译器在运行状态下引入了高效的环回收机制,允许有选择的智能回收某个APP的内存占用,这对传统的环回收算法是一个非常棒的改进。
华为很快将全面开源方舟编译器,届时开发者们可以使用方舟编译器作为开发工具,为移动应用生态和体验打开一扇全新的大门。科技的进步,离不开全球无数科学家和工程师的奉献。产业的繁荣,开放是唯一出路! 最后,以Linux 的创始人Linus Torvalds的名言结尾!
Talk is cheap. Show me the code!
空谈误国,实干兴邦!
欢迎关注 留言交流